<template>
	<ScrollArea
		ref="scrollAreaRef"
		v-slot="{containerSize:{width,height},position:{y}}"
		v-bind="$attrs"
		class="pf-Menu"
	>
		<VirtualScroll
			ref="virtualScrollRef"
			v-slot="slotScope"
			:data="treeInfo.visibleFlatList"
			:data-map="dataMapFn"
			direction="y"
			:min-size="virtualMinSize"
			:preload="virtualPreload"
			:scroll-offset="y"
			:view-size="height"
			:style="{width:width+'px'}"
		>
			<div
				:key="slotScope.item.keyStr"
				:class="itemClasses(slotScope)"
				@click="clickFn(slotScope.item,$event)"
			>
				<div
					class="pf-Menu_content"
					:style="{marginLeft:slotScope.item.indent*22+'px'}"
				><slot
					:name="slotName(slotScope)"
					v-bind="slotScope"
					:active="innerActive"
					:active-path="activePath"
				>{{slotScope.value}}</slot></div>
				<slot
					v-if="slotScope.item.lazyLoading"
					v-bind="slotScope"
					name="loading"
				><Spin
					:type="loadingType"
					class="pf-Menu_spin"
				/></slot>
				<slot
					v-else-if="slotScope.item.notLeaf"
					v-bind="slotScope"
					name="arrow"
				><Icon
					:name="arrowIcon"
					:class="iconClasses(slotScope)"
				/></slot>
			</div>
		</VirtualScroll>
	</ScrollArea>
</template>

<script setup lang="ts">
import type {
	PropOrDescriptor,
	PropOrGetter,
	FieldGetter,
	BeforeProcessor,
	MovePosition,
} from '../../../type';
import type {
	MenuItem,
	extraMeta,
	ResolvedRoute,
} from './type';
import type {
	TreeLazyLoad,
	TreeNodeMeta,
	TreeKey,
} from '../../../composables/useTree/type';
import type {SpinType} from '../Spin/type';
import type {VirtualScrollItemScope} from '../VirtualScroll/type';
import type {RouteLocationRaw} from 'vue-router';
import {
	shallowRef,
	triggerRef,
	toRef,
	computed,
	watch,
	nextTick,
} from 'vue';
import {useRoute,useRouter} from 'vue-router';
import ScrollArea from '../ScrollArea/ScrollArea.vue';
import VirtualScroll from '../VirtualScroll/VirtualScroll.vue';
import Icon from '../Icon/Icon';
import Spin from '../Spin/Spin.vue';
import useTree from '../../../composables/useTree';
import {MySet,routeIncludesParams,throttle,createGetter} from '../../../util';
/**
 * fixme VirtualScroll插槽内容存在效率问题
 * todo 动画效果
 * 菜单组件
 * @author 唐国雄
 * @date 2022/10/15
 * 属性----------------------
 * :data					菜单数据
 * :obj-key					唯一键
 * :format					格式化
 * :label-key				显示属性
 * :children-key			子菜单属性
 * :route-key				路由属性
 * :replace					使用replace而不是push
 * :href-key				跳转链接(优先于路由)
 * :accordion				手风琴模式
 * :expanded				已展开菜单
 * :active					当前激活的菜单
 * :auto-expand				自动展开到激活的菜单
 * :before-active			激活前置处理器
 * :lazy-load				懒加载方法
 * :arrow-icon				箭头图标
 * :loading-type			懒加载图标类型
 * :virtual-min-size		菜单最小高度
 * :virtual-preload			虚拟滚动预加载量
 * 事件----------------------
 * @update:active			激活菜单
 * @expand					展开菜单(用户点击触发)
 * @collapse				折叠菜单(用户点击触发)
 * 插槽----------------------
 * #loading					加载中插槽
 * #arrow					箭头插槽
 * #item					内容插槽
 * #item-[name]				特定内容插槽
 * 方法----------------------
 * getMeta(key)						通过key获取节点信息
 * getMetaByKeyOrRaw(keyOrRaw)		通过key或者raw获取节点信息
 * setExpand({state,keyOrRaw?,meta?,isAccordion?})			设置行的展开状态(带校验), 返回操作是否成功
 * setExpands({state,keyOrRaws,isAccordion?})				批量设置行的展开状态(带校验)
 * setExpandsAll({state})									设置所有行的展开状态(带校验)
 * expandByPropMark()										展开所有_expand为true的节点
 * expandToNode({keyOrRaw?,meta?,isAccordion?})				展开到节点
 * scrollTo({keyOrRaw?,meta?,isAccordion?,pos?,duration?})	滚动到节点
 * getExpanded({expanded?,raw?})	获取展开的行数据
 * clearLazyCache()					清空懒加载状态
 */
const emit=defineEmits<{
	(e:'update:active',key:MenuItem):void;
	(e:'expand',raw:MenuItem):void;
	(e:'collapse',raw:MenuItem):void;
}>();
const props=withDefaults(defineProps<{
	data:Array<MenuItem>;
	objKey?:PropOrGetter<MenuItem>|null;
	format?:FieldGetter<string>;
	labelKey?:PropOrGetter<MenuItem>;
	childrenKey?:PropOrDescriptor<MenuItem,MenuItem[]>;
	routeKey?:PropOrGetter<MenuItem,RouteLocationRaw>;
	replace?:boolean;
	hrefKey?:PropOrGetter<MenuItem>;
	accordion?:boolean;
	expanded?:MySet<TreeKey<MenuItem>>;
	active?:TreeKey<MenuItem>;
	autoExpand?:boolean;
	beforeActive?:BeforeProcessor<TreeNodeMeta<MenuItem,extraMeta>>;
	lazyLoad?:TreeLazyLoad<MenuItem,extraMeta>;
	arrowIcon?:string;
	loadingType?:SpinType;
	virtualMinSize?:number;
	virtualPreload?:number;
}>(),{
	labelKey:'label',
	childrenKey:'children',
	routeKey:'to',
	hrefKey:'href',
	autoExpand:true,
	arrowIcon:'bi-chevron-down',
	virtualMinSize:48,
	expanded(){
		return new MySet();
	},
});
const scrollAreaRef=shallowRef(null);
const virtualScrollRef=shallowRef(null);
const innerActive=shallowRef<TreeKey<MenuItem>>();
const activePath=shallowRef<Set<TreeKey<MenuItem>>>(new Set());
const router=useRouter();
const curRoute=router&&useRoute();
//region 树相关功能
const {
	getKeyByKeyOrRaw,
	treeInfo,
	getMetaByKeyOrRaw,
	setExpand,
	setExpands,
	setExpandsAll,
	expandByPropMark,
	expandToNode,
	getExpanded,
	clearLazyCache,
}=useTree<MenuItem,extraMeta>({
	objKey:toRef(props,'objKey'),
	childrenKey:toRef(props,'childrenKey'),
	accordion:toRef(props,'accordion'),
	data:toRef(props,'data'),
	lazyProp:'_lazy',
	expandableProp:'_expandable',
	expandProp:'_expand',
	leafProp:'_leaf',
	expandedSet:props.expanded,
	visibleFilterFn(meta){
		return meta.visible;
	},
	lazyLoadFn:props.lazyLoad,
	injectionLoopFn(){
		const getRouteFn=createGetter(props.routeKey);
		const getHrefFn=createGetter(props.hrefKey);
		return {
			initFn(meta){
				if(!meta.notLeaf){
					const {raw}=meta;
					meta.href=getHrefFn(raw);
					let resolvedRoute:ResolvedRoute|undefined;
					let to:RouteLocationRaw|undefined;
					if(!meta.href&&router!==undefined){
						to=getRouteFn(raw);
						if(to){
							resolvedRoute=meta.to!==to?router.resolve(to):meta.resolvedRoute;
							let activeRecordIndex=(function():number{
								const {matched}=resolvedRoute;
								const {length}=matched;
								const routeMatched=matched[length-1];
								const currentMatched=curRoute.matched;
								if(!routeMatched||!currentMatched.length) return -1;
								return currentMatched.indexOf(routeMatched);
							})();
							if(
								activeRecordIndex>-1&&
								routeIncludesParams(curRoute.params,resolvedRoute.params)
							){
								innerActive.value=meta.key;
							}
						}
					}
					meta.to=to;
					meta.resolvedRoute=resolvedRoute;
				}
			},
		};
	},
});
//endregion
//region 数据映射方法
const dataMapFn=computed(() => {
	const {labelKey,format}=props;
	const getLabelFn=createGetter(labelKey);
	return (oldScope:VirtualScrollItemScope) => {
		let value=getLabelFn(oldScope.item.raw);
		if(format){
			value=format(value);
		}
		const scope:any=oldScope;
		scope.value=value;
		return scope;
	};
});
//endregion
//region 菜单类名
function itemClasses({item}){
	return 'pf-Menu_item'+
		(innerActive.value===item.key
			?' pf--active'
			:activePath.value.has(item.key)
				?' pf--active-path'
				:'')+
		(item.raw._disabled?' pf--disabled':'')+
		(item.notLeaf&&!item.expandable?' pf--expandable-false':'');
}
//endregion
//region 展开图标类名
function iconClasses({item}){
	return 'pf-Menu_icon'+
		(item.expanded?' pf--expanded':'');
}
//endregion
//region 插槽名
function slotName({item:{raw:{_slot}}}){
	return _slot?`item-${_slot}`:'item';
}
//endregion
//region 点击事件处理器
function clickFn(meta:TreeNodeMeta<MenuItem,extraMeta>,e:MouseEvent){
	if(meta.notLeaf){
		const {raw,expanded}=meta;
		setExpand({
			state:!expanded,
			meta,
		}).then((success) => {
			if(success){
				if(expanded){
					emit('collapse',raw);
				}else{
					emit('expand',raw);
				}
			}
		});
	}else{
		const {raw,key}=meta;
		if(raw._disabled||innerActive.value===key){
			e.preventDefault();
			return;
		}
		Promise.resolve(props.beforeActive?.(meta)).then((success) => {
			if(success===false){
				throw 'beforeActive阻止';
			}
			const {href,to,resolvedRoute}=meta;
			const {_target}=raw;
			if(href){
				window.open(href,_target);
			}else if(resolvedRoute){
				if(
					_target==='_blank'||
					(_target==='_parent'&&parent!==window)||
					(_target==='_top'&&top!==window)
				){
					window.open(resolvedRoute.href,_target);
				}else{
					const {_replace}=raw;
					const method=(_replace==null?props.replace:_replace)
						?'replace'
						:'push';
					return router[method](to);
				}
			}
		}).then(() => {
			innerActive.value=key;
		}).catch(function(){});
	}
}
//endregion
//region 滚动到节点
function scrollTo(
	{
		keyOrRaw,
		meta,
		isAccordion=props.accordion,
		pos='nearest',
		duration,
	}:{
		keyOrRaw?:TreeKey<MenuItem>;
		meta?:TreeNodeMeta<MenuItem,extraMeta>;
		isAccordion?:boolean;
		pos?:MovePosition;
		duration?:number;
	}
):Promise<void>{
	if(!meta){
		meta=getMetaByKeyOrRaw(keyOrRaw!);
	}
	expandToNode({
		meta,
		isAccordion,
	});
	return nextTick().then(() => {
		const scrollArea=scrollAreaRef.value;
		const virtualScroll=virtualScrollRef.value;
		const index=treeInfo.value.visibleFlatList.indexOf(meta);
		if(pos==='nearest'){
			const {offset,itemSize}=virtualScroll.getInfo(index,'start');
			const {containerSize,position}=scrollArea.getInfo();
			const curScrollOffset=position.y;
			const viewSize=containerSize.height;
			if(offset<curScrollOffset){
				return scrollArea.setScrollPosition('y',offset,duration);
			}else if(offset+itemSize>curScrollOffset+viewSize){
				return scrollArea.setScrollPosition('y',offset-(viewSize-itemSize),duration);
			}
			return Promise.resolve();
		}
		return scrollArea.setScrollPosition(
			'y',
			virtualScroll.getInfo(index,pos).offset,
			duration,
		);
	});
}
//endregion
//region 当新的菜单激活, 执行处理
let lastParent;
const _dealActiveFn=throttle((meta:TreeNodeMeta<MenuItem,extraMeta>) => {
	if(lastParent!==meta.parent){
		const set=activePath.value;
		set.clear();
		let curMeta=lastParent=meta.parent;
		while(curMeta){
			set.add(curMeta.key);
			curMeta=curMeta.parent;
		}
		triggerRef(activePath);
	}
	props.autoExpand&&scrollTo({
		meta,
	});
});
//endregion
watch(innerActive,(active) => {
	const meta=treeInfo.value.getMeta(active);
	if(meta){
		emit('update:active',meta.raw);
		_dealActiveFn(meta);
	}
});
watch(() => props.active,(active) => {
	innerActive.value=getKeyByKeyOrRaw(active);
},{immediate:true});
//region 导出的内容
defineExpose({
	getMeta(key:TreeKey<MenuItem>){
		return treeInfo.value.getMeta(key);
	},
	getMetaByKeyOrRaw,
	setExpand,
	setExpands,
	setExpandsAll,
	expandByPropMark,
	expandToNode,
	scrollTo,
	getExpanded,
	clearLazyCache,
});
//endregion
</script>

<script lang="ts">
export default {
	inheritAttrs:false,
};
</script>